package cn.zz.comm.web.exception;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

import org.springframework.beans.propertyeditors.StringTrimmerEditor;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;

import cn.zz.comm.exception.BizErrorCode;
import cn.zz.comm.exception.BizException;
import cn.zz.comm.exception.enums.SysErrorCode;
import cn.zz.comm.types.result.R;
import cn.zz.comm.util.ValidatorUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;

/**
 * @author zhongq
 * @since 2023/01/09
 */
@Slf4j
@RestControllerAdvice
public class WebExceptionAdvice {

	// -------- init binder --------

	@InitBinder
	public void initBinder(WebDataBinder binder) {
		binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
	}
	// -------- exception handler --------

	// 400
	@ExceptionHandler(BindException.class)
	@ResponseBody
	@ResponseStatus(HttpStatus.BAD_REQUEST)
	public R<?> bindException(BindException e) {
		return bindErrorToBizException(e, e.getBindingResult());
	}

	@ExceptionHandler(MethodArgumentNotValidException.class)
	@ResponseStatus(HttpStatus.BAD_REQUEST)
	public R<?> manvException(MethodArgumentNotValidException e) {
		return bindErrorToBizException(e, e.getBindingResult());
	}

	@ExceptionHandler(ConstraintViolationException.class)
	@ResponseStatus(HttpStatus.BAD_REQUEST)
	public R<?> cvException(ConstraintViolationException e) {
		String details = e.getConstraintViolations().stream().map(ValidatorUtil::getMessageFromConstraintViolation)
				.collect(Collectors.joining(", ", "[", "]"));
		return error(e, SysErrorCode.PARAM_INVALID, details);
	}

	@ExceptionHandler(HttpMessageNotReadableException.class)
	@ResponseStatus(HttpStatus.BAD_REQUEST)
	public R<?> hmnrException(HttpMessageNotReadableException e) {
		return error(e, SysErrorCode.PARAM_INVALID, "malformed http message");
	}

	@ExceptionHandler(MissingServletRequestParameterException.class)
	@ResponseStatus(HttpStatus.BAD_REQUEST)
	public R<?> missParam(MissingServletRequestParameterException e) {
		return error(e, SysErrorCode.PARAM_INVALID, "required param is not present: " + e.getParameterName());
	}

	@ExceptionHandler(MethodArgumentTypeMismatchException.class)
	@ResponseStatus(HttpStatus.BAD_REQUEST)
	public R<?> typeMismatch(MethodArgumentTypeMismatchException e) {
		return error(e, SysErrorCode.PARAM_INVALID, "type mismatch for param: " + e.getName());
	}

	@ExceptionHandler(BizException.class)
	public R<?> bizException(BizException e) {
		return R.fail(e);
	}

	@ResponseStatus(value = HttpStatus.NOT_FOUND)
	@ExceptionHandler(NoHandlerFoundException.class)
	public R<?> noHandlerFound(NoHandlerFoundException e) {
		return error(e, SysErrorCode.HANDLER_NOT_FOUND, e.getHttpMethod(), e.getRequestURL());
	}

	// // 405
	// @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
	// @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
	// public R<?> methodError(HttpRequestMethodNotSupportedException e) {
	//
	// return error(new BizException(HttpStatus.METHOD_NOT_ALLOWED.value() * 100,
	// "ERROR",
	// "http method {} is not allowed", new String[] { e.getMethod() }) {
	// private static final long serialVersionUID = 1L;
	// });
	// }

	// @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
	// @ResponseBody
	// @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
	// public R<?> mediaTypeError(HttpMediaTypeNotSupportedException e) {
	// return error(new BizException(HttpStatus.UNSUPPORTED_MEDIA_TYPE.value() *
	// 100, "ERROR",
	// "media type {} is not allowed", new String[] { e.getContentType().toString()
	// }) {
	// private static final long serialVersionUID = 1L;
	// });
	// }
	@ExceptionHandler(Exception.class)
	public R<?> defaultException(HttpServletRequest request, HttpServletResponse response, Exception e) {
		log.info("run into uncovered exception: {}", e.getMessage(), e);
		// error message
		String errmsg = request.getMethod() + "_" + request.getRequestURI() + "->failed@" + System.currentTimeMillis()
				+ "^" + e.getClass().getSimpleName() + "^" + Thread.currentThread().getName();
		// request params
		Map<String, String[]> requestParams = request.getParameterMap();
		Map<String, String> params = new HashMap<>(requestParams.size(), 1F);
		requestParams.forEach((key, valueArray) -> {
			String value = (valueArray == null) ? "" : String.join("|", valueArray);
			params.put(key, value);
		});
		String stes = Arrays.stream(e.getStackTrace()).map(StackTraceElement::toString).filter(s -> s.contains("tmp"))
				.map(s -> "    " + s).collect(Collectors.joining("\n"));
		log.warn("uncovered exception, {}, params: {}, exp: {}, stackTrace: \n{}", errmsg, params, e.getMessage(),
				stes);
		// package response
		response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
		// response.setHeader(RequestHeaders.requestId,
		// Thread.currentThread().getName());
		return error(e, SysErrorCode.FATAL_ERROR, errmsg);
	}

	@ExceptionHandler(Throwable.class)
	public R<?> throwable(Throwable e) {
		return error(e, SysErrorCode.SYSTEM_ERROR);
	}

	// helper
	private R<?> bindErrorToBizException(Exception e, BindingResult bindingResult) {
		String details = bindingResult.getAllErrors().stream().map(oe -> {
			if (oe instanceof FieldError foe) {
				return foe.getField() + "->" + foe.getDefaultMessage();
			} else {
				return oe.getDefaultMessage();
			}
		}).collect(Collectors.joining(", ", "[", "]"));
		return error(e, SysErrorCode.PARAM_INVALID, details);
	}

	private R<?> error(Throwable e, BizErrorCode code, Object... params) {
		final BizException exception = new BizException(code, params);
		log.error(" code {}, message {} ", exception.getCode(), e.getMessage(), e);
		return R.fail(exception);
	}

}